Skip to content

Commit

Permalink
Sketched password login API calls.
Browse files Browse the repository at this point in the history
Access tokens flattened in GraphQL schema.
Fixed several RxJS7 deprecations on the way
  • Loading branch information
zaychenko-sergei committed Aug 24, 2023
1 parent 9b715b7 commit 60944c3
Show file tree
Hide file tree
Showing 12 changed files with 250 additions and 71 deletions.
9 changes: 2 additions & 7 deletions resources/schema.graphql
Original file line number Diff line number Diff line change
@@ -1,9 +1,3 @@
type AccessToken {
accessToken: String!
scope: String!
tokenType: String!
}

interface Account {
id: AccountID!
name: AccountName!
Expand Down Expand Up @@ -52,6 +46,7 @@ type AttachmentsEmbedded {
}

type AuthMut {
passwordLogin(login: String!, password: String!): LoginResponse!
githubLogin(code: String!): LoginResponse!
accountInfo(accessToken: String!): AccountInfo!
}
Expand Down Expand Up @@ -483,7 +478,7 @@ type InputSlice {
}

type LoginResponse {
token: AccessToken!
accessToken: String!
accountInfo: AccountInfo!
}

Expand Down
103 changes: 84 additions & 19 deletions src/app/api/auth.api.spec.ts
Original file line number Diff line number Diff line change
@@ -1,15 +1,23 @@
import { fakeAsync, flush, TestBed, tick } from "@angular/core/testing";
import { Apollo } from "apollo-angular";
import { AuthApi } from "./auth.api";
import { AccountDetailsFragment, FetchAccountInfoDocument, GithubLoginDocument } from "./kamu.graphql.interface";
import {
AccountDetailsFragment,
FetchAccountInfoDocument,
GithubLoginDocument,
PasswordLoginDocument,
} from "./kamu.graphql.interface";
import { ApolloTestingController, ApolloTestingModule } from "apollo-angular/testing";
import {
mockAccountDetails,
mockGithubLoginResponse,
mockLogin401Error,
mockPasswordLoginResponse,
mockUserInfoFromAccessToken,
TEST_ACCESS_TOKEN,
TEST_ACCESS_TOKEN_GITHUB,
TEST_GITHUB_CODE,
TEST_LOGIN,
TEST_PASSWORD,
} from "./mock/auth.mock";
import { AuthenticationError } from "../common/errors";
import { first } from "rxjs/operators";
Expand All @@ -32,10 +40,10 @@ describe("AuthApi", () => {
});

function loginViaAccessToken(): void {
service.fetchUserInfoFromAccessToken(TEST_ACCESS_TOKEN).subscribe();
service.fetchUserInfoFromAccessToken(TEST_ACCESS_TOKEN_GITHUB).subscribe();

const op = controller.expectOne(FetchAccountInfoDocument);
expect(op.operation.variables.accessToken).toEqual(TEST_ACCESS_TOKEN);
expect(op.operation.variables.accessToken).toEqual(TEST_ACCESS_TOKEN_GITHUB);

op.flush({
data: mockUserInfoFromAccessToken,
Expand All @@ -53,16 +61,73 @@ describe("AuthApi", () => {
});
}

function loginFullyViaPassword(): void {
service.fetchUserInfoAndTokenFromPasswordLogin(TEST_LOGIN, TEST_PASSWORD).subscribe();

const op = controller.expectOne(PasswordLoginDocument);
expect(op.operation.variables.login).toEqual(TEST_LOGIN);
expect(op.operation.variables.password).toEqual(TEST_PASSWORD);

op.flush({
data: mockPasswordLoginResponse,
});
}

it("should be created", () => {
expect(service).toBeTruthy();
});

it("should check full login GraphQL success", fakeAsync(() => {
it("should check full login password success", fakeAsync(() => {
const accessTokenObtained$ = service
.accessTokenObtained()
.pipe(first())
.subscribe((token: string) => {
expect(token).toEqual(mockGithubLoginResponse.auth.githubLogin.token.accessToken);
expect(token).toEqual(mockPasswordLoginResponse.auth.passwordLogin.accessToken);
});

const accountChanged$ = service
.accountChanged()
.pipe(first())
.subscribe((user: AccountDetailsFragment) => {
expect(user).toEqual(mockAccountDetails);
});

loginFullyViaPassword();
tick();

expect(accessTokenObtained$.closed).toBeTrue();
expect(accountChanged$.closed).toBeTrue();
flush();
}));

it("should check full login password failure", fakeAsync(() => {
const subscription$ = service
.fetchUserInfoAndTokenFromPasswordLogin(TEST_LOGIN, TEST_PASSWORD)
.pipe(first())
.subscribe({
next: () => fail("Unexpected success"),
error: (e: Error) => {
expect(e).toEqual(new AuthenticationError([mockLogin401Error]));
},
});

const op = controller.expectOne(PasswordLoginDocument);
expect(op.operation.variables.login).toEqual(TEST_LOGIN);
expect(op.operation.variables.password).toEqual(TEST_PASSWORD);

op.graphqlErrors([mockLogin401Error]);
tick();

expect(subscription$.closed).toBeTrue();
flush();
}));

it("should check full login Github success", fakeAsync(() => {
const accessTokenObtained$ = service
.accessTokenObtained()
.pipe(first())
.subscribe((token: string) => {
expect(token).toEqual(mockGithubLoginResponse.auth.githubLogin.accessToken);
});

const accountChanged$ = service
Expand All @@ -80,16 +145,16 @@ describe("AuthApi", () => {
flush();
}));

it("should check full login GraphQL failure", fakeAsync(() => {
it("should check full login Github failure", fakeAsync(() => {
const subscription$ = service
.fetchUserInfoAndTokenFromGithubCallackCode(TEST_GITHUB_CODE)
.pipe(first())
.subscribe(
() => fail("Unexpected success"),
(e: Error) => {
.subscribe({
next: () => fail("Unexpected success"),
error: (e: Error) => {
expect(e).toEqual(new AuthenticationError([mockLogin401Error]));
},
);
});

const op = controller.expectOne(GithubLoginDocument);
expect(op.operation.variables.code).toEqual(TEST_GITHUB_CODE);
Expand All @@ -101,7 +166,7 @@ describe("AuthApi", () => {
flush();
}));

it("should check login via access token GraphQL success", fakeAsync(() => {
it("should check login via access token success", fakeAsync(() => {
const accessTokenObtained$ = service
.accessTokenObtained()
.pipe(first())
Expand All @@ -126,19 +191,19 @@ describe("AuthApi", () => {
flush();
}));

it("should check login via access token GraphQL failure", fakeAsync(() => {
it("should check login via access token failure", fakeAsync(() => {
const subscription$ = service
.fetchUserInfoFromAccessToken(TEST_ACCESS_TOKEN)
.fetchUserInfoFromAccessToken(TEST_ACCESS_TOKEN_GITHUB)
.pipe(first())
.subscribe(
() => fail("Unexpected success"),
(e: Error) => {
.subscribe({
next: () => fail("Unexpected success"),
error: (e: Error) => {
expect(e).toEqual(new AuthenticationError([mockLogin401Error]));
},
);
});

const op = controller.expectOne(FetchAccountInfoDocument);
expect(op.operation.variables.accessToken).toEqual(TEST_ACCESS_TOKEN);
expect(op.operation.variables.accessToken).toEqual(TEST_ACCESS_TOKEN_GITHUB);

op.graphqlErrors([mockLogin401Error]);
tick();
Expand Down
29 changes: 25 additions & 4 deletions src/app/api/auth.api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@ import {
FetchAccountInfoMutation,
GithubLoginGQL,
GithubLoginMutation,
PasswordLoginGQL,
PasswordLoginMutation,
} from "./kamu.graphql.interface";
import { MutationResult } from "apollo-angular";
import { AuthenticationError } from "../common/errors";
Expand All @@ -15,7 +17,11 @@ import { AuthenticationError } from "../common/errors";
providedIn: "root",
})
export class AuthApi {
constructor(private githubLoginGQL: GithubLoginGQL, private fetchAccountInfoGQL: FetchAccountInfoGQL) {}
constructor(
private githubLoginGQL: GithubLoginGQL,
private passwordLoginGQL: PasswordLoginGQL,
private fetchAccountInfoGQL: FetchAccountInfoGQL,
) {}

private accessTokenObtained$: Subject<string> = new ReplaySubject<string>(1);
private accountChanged$: Subject<AccountDetailsFragment> = new ReplaySubject<AccountDetailsFragment>(1);
Expand All @@ -28,18 +34,33 @@ export class AuthApi {
return this.accountChanged$.asObservable();
}

public fetchUserInfoAndTokenFromPasswordLogin(login: string, password: string): Observable<void> {
return this.passwordLoginGQL.mutate({ login, password }).pipe(
map((result: MutationResult<PasswordLoginMutation>) => {
if (result.data) {
const data: PasswordLoginMutation = result.data;
this.accessTokenObtained$.next(data.auth.passwordLogin.accessToken);
this.accountChanged$.next(data.auth.passwordLogin.accountInfo);
} else {
throw new AuthenticationError(result.errors ?? []);
}
}),
catchError((e: Error) => throwError(() => new AuthenticationError([e]))),
);
}

public fetchUserInfoAndTokenFromGithubCallackCode(code: string): Observable<void> {
return this.githubLoginGQL.mutate({ code }).pipe(
map((result: MutationResult<GithubLoginMutation>) => {
if (result.data) {
const data: GithubLoginMutation = result.data;
this.accessTokenObtained$.next(data.auth.githubLogin.token.accessToken);
this.accessTokenObtained$.next(data.auth.githubLogin.accessToken);
this.accountChanged$.next(data.auth.githubLogin.accountInfo);
} else {
throw new AuthenticationError(result.errors ?? []);
}
}),
catchError((e: Error) => throwError(new AuthenticationError([e]))),
catchError((e: Error) => throwError(() => new AuthenticationError([e]))),
);
}

Expand All @@ -53,7 +74,7 @@ export class AuthApi {
throw new AuthenticationError(result.errors ?? []);
}
}),
catchError((e: Error) => throwError(new AuthenticationError([e]))),
catchError((e: Error) => throwError(() => new AuthenticationError([e]))),
);
}
}
15 changes: 11 additions & 4 deletions src/app/api/gql/github-login.graphql
Original file line number Diff line number Diff line change
@@ -1,11 +1,18 @@
mutation GithubLogin($code: String!) {
auth {
githubLogin(code: $code) {
token {
accessToken
scope
tokenType
accessToken
accountInfo {
...AccountDetails
}
}
}
}

mutation PasswordLogin($login: String!, $password: String!) {
auth {
passwordLogin(login: $login, password: $password) {
accessToken
accountInfo {
...AccountDetails
}
Expand Down
Loading

0 comments on commit 60944c3

Please sign in to comment.